﻿using System;
using System.Linq;
using RenderEngineInterfaces;
using System.ComponentModel.Composition;
using System.IO;
using System.Threading.Tasks;
using DeepZoomPivotConstructor.VisualLib;
using System.Net;
using System.Text.RegularExpressions;
using System.Text;
using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using System.Threading;

namespace UriRenderer
{
    /// <summary>
    /// Download a file from the net, and then send it off
    /// to be rendered by the rest of the system.
    /// </summary>
    [Export(typeof(IRenderItem))]
    [RenderInfo(typeof(Uri))]
    public class RenderUri : IRenderItem, IRenderableObjectCompare
    {
        /// <summary>
        /// Create the future that will do the actual work of downloading and
        /// rendering the file.
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public Task<Visual> Render(object what)
        {
            return Task<Visual>.Factory.StartNew(() => DownloadAndRender(what as Uri));
        }

        /// <summary>
        /// So we can render the file that is downloaded!
        /// </summary>
        [Import]
        private IRenderEngine MyRender { get; set; }

        /// <summary>
        /// If we should be using a different temp directory than the one that is there by default... :-)
        /// </summary>
#pragma warning disable 0649
        [Import("RenderingTempDir", AllowDefault = true)]
        private DirectoryInfo RenderingTempDir;
#pragma warning restore 0649

        /// <summary>
        /// Make sure to limit the # of simultanious downloads!!
        /// </summary>
        static Semaphore _guardNumberOfDownloads = new Semaphore(2, 2);

        private System.Collections.Generic.HashSet<string> _seenURLs = new System.Collections.Generic.HashSet<string>();

        /// <summary>
        /// Do the actual rendering. We are on a background thread when this guy starts.
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        private Visual DownloadAndRender(Uri uri)
        {
            ///
            /// Certian things we should not download!
            /// 

            if (uri.OriginalString.StartsWith("http://cern.ch/cernaccount"))
            {
                return null;
            }

            ///
            /// Next, have we seen this before?
            /// 

            lock (_seenURLs)
            {
                if (_seenURLs.Contains(uri.OriginalString))
                {
                    return null;
                }
                _seenURLs.Add(uri.OriginalString);
            }

            ///
            /// Ok. Now do the actual download. If we get a timeout error, try again (yeah, I know... but...).
            /// Make sure that we only do a download if we are "allowed" to do a download (keep the # of simultanious downloads
            /// to a minimum).
            /// 

            int timeoutRetryes = 3;

            FileInfo finalFile = null;

            while (true)
            {
                WebRequest req = WebRequest.Create(uri);
                (req as HttpWebRequest).UserAgent = HttpAgentString;
                try
                {
                    _guardNumberOfDownloads.WaitOne();
                    using (WebResponse res = req.GetResponse())
                    {
                        ///
                        /// Figure out the filetype...
                        /// 

                        string fileType = GetFileType(res);
                        if (fileType == null)
                        {
                            return null;
                        }

                        finalFile = new FileInfo(Path.GetTempPath() + Path.DirectorySeparatorChar + Path.GetRandomFileName());
                        if (RenderingTempDir != null)
                        {
                            finalFile = new FileInfo(RenderingTempDir.FullName + Path.DirectorySeparatorChar + finalFile.Name);
                        }
                        finalFile = new FileInfo(Path.ChangeExtension(finalFile.FullName, fileType));

                        ///
                        /// Ok - now prep the read loop
                        /// 

                        float filesize = res.ContentLength; // Saftey in case we can't get it.
                        Stream datastream = res.GetResponseStream();
                        datastream.ReadTimeout = 1024 * 10;

                        const int datasize = 1024 * 16;
                        byte[] data = new byte[datasize];
                        int bytesread = 0;

                        using (Stream output = finalFile.Create())
                        {

                            bool done = false;
                            while (!done)
                            {
                                int readbytes = datastream.Read(data, 0, datasize);
                                if (readbytes == 0)
                                {
                                    done = true;
                                }
                                else
                                {
                                    bytesread = bytesread + readbytes;
                                    output.Write(data, 0, readbytes);
                                }
                            }

                            output.Close();
                        }

                        datastream.Close();
                    }
                    break;
                }
                catch (Exception e)
                {
                    MyErrorLogger.LogMessage("RenderUri", "Download of url " + uri.OriginalString + " failed!", e);
                    if (e.Message.Contains("Timeout") || e.Message.Contains("timeout"))
                    {
                        timeoutRetryes--;
                        if (timeoutRetryes <= 0)
                        {
                            return null;
                        }
                        MyErrorLogger.LogMessage("RenderUri", "Will retry url " + uri.OriginalString + ".");
                    }
                    else
                    {
                        return null;
                    }
                }
                finally
                {
                    _guardNumberOfDownloads.Release();
                }
            }

            ///
            /// If this is a PNG file make sure that the DPI of the file is correct.
            /// 

            if (finalFile.Extension == ".png")
            {
                using (var img = System.Drawing.Image.FromFile(finalFile.FullName))
                {
                    if (img.HorizontalResolution != img.VerticalResolution)
                    {
                        MyErrorLogger.LogMessage("RenderUri", "DPI x and y don't match - so aborting download of " + uri.OriginalString);
                        return null;
                    }
                }
            }

            ///
            /// Finally, if this was an html file, we need to update relative lines. Ouch!
            /// It feels ugly to have this living here, but I'm not sure how else to do it.
            /// 

            if (finalFile.Extension == ".html")
            {
                string lastGuy = uri.Segments.Last();
                string baseUri = uri.ToString();
                if (lastGuy.Contains('.'))
                {
                    baseUri = baseUri.Substring(0, baseUri.LastIndexOf('/'));
                }
                if (!lastGuy.EndsWith("/"))
                {
                    lastGuy = lastGuy + "/";
                }
                
                finalFile = UpdateRelativeLines(finalFile, baseUri);
            }

            ///
            /// Fix up the date, if we can
            /// 

            long ticks = GetLastModifiedTime(uri);
            if (ticks > 0)
            {
                DateTime lastMod = new DateTime(ticks);
                if (lastMod.Year > 1980)
                {
                    finalFile.LastWriteTime = lastMod;
                }
            }

            ///
            /// We have the file. Rename it, and then render it!
            /// 

            return MyRender.Render(finalFile);

        }

        /// <summary>
        /// We have downloaded an html file. Update the relative lines.
        /// This is basically a transcription. NOTE: we expect all href to appear on a single line.
        /// 
        /// Avoid mailto: links!
        /// </summary>
        /// <param name="finalFile"></param>
        /// <returns></returns>
        public FileInfo UpdateRelativeLines(FileInfo htmlFile, string baseUri)
        {
            FileInfo result = new FileInfo(Path.ChangeExtension(Path.GetTempFileName(), htmlFile.Extension));

            Uri baseU = new Uri(baseUri);
            string uriFull = baseU.AbsoluteUri;
            if (!uriFull.EndsWith("/"))
            {
                uriFull += "/";
            }
            string uriSite = baseU.Scheme + "://" + baseU.DnsSafeHost;
 
            using (var reader = htmlFile.OpenText())
            using (var writer = result.CreateText())
            {
                Regex hrefFinder = new Regex("href=\"([^\"]+)\"");
                string line = null;
                while ((line = reader.ReadLine()) != null)
                {
                    StringBuilder bld = new StringBuilder();
                    var match = hrefFinder.Match(line);
                    int firstNotWrittenWrittenOffset = 0;
                    while (match.Success)
                    {
                        int delta = match.Index - firstNotWrittenWrittenOffset;
                        bld.Append(line.Substring(firstNotWrittenWrittenOffset, delta));

                        bld.Append("href=\"");
                        string link = match.Groups[1].Value;
                        if (!link.Contains(":"))
                        {
                            if (link.StartsWith("/"))
                            {
                                bld.Append(uriSite);
                            }
                            else
                            {
                                bld.Append(uriFull);
                            }
                        }
                        bld.Append(link);
                        bld.Append("\"");

                        firstNotWrittenWrittenOffset = match.Index + match.Length;

                        match = match.NextMatch();
                    }

                    bld.Append(line.Substring(firstNotWrittenWrittenOffset));

                    writer.WriteLine(bld.ToString());
                }
            }

            ///
            /// Done - we go back now!
            /// 

            return result;
        }

        [Import]
        private IErrorLogger MyErrorLogger { get; set; }

        /// <summary>
        /// The agent string we use in URI requests!
        /// </summary>
        [Import("HttpAgentString")]
        private string HttpAgentString { get; set; }

        /// <summary>
        /// Grab the header file and see if it is the correct type
        /// (or not).
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        private string GetFileType(Uri uri)
        {
            WebRequest req = WebRequest.Create(uri);
            req.Method = "HEAD";
            (req as HttpWebRequest).UserAgent = HttpAgentString;
            try
            {
                using (WebResponse res = req.GetResponse())
                {
                    return GetFileType(res);
                }
            }
            catch (Exception e)
            {
                MyErrorLogger.LogMessage("RenderUri", "Error getting the filetype for uri " + uri.ToString(), e);
                return null;
            }
        }

        /// <summary>
        /// Given a web response, return a file type
        /// </summary>
        /// <param name="res"></param>
        /// <returns></returns>
        private string GetFileType(WebResponse res)
        {
            if (isContent("pdf", res))
            {
                return ".pdf";
            }
            else if (isContent("ppt", "powerpoint", res))
            {
                return ".ppt";
            }
            else if (isContent("ps", "postscript", res))
            {
                return ".ps";
            }
            else if (isContent("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation", res))
            {
                return ".pptx";
            }
            else if (isContent("jpg", "image/jpeg", res))
            {
                return ".jpg";
            }
            else if (isContent("png", "image/png", res))
            {
                return ".png";
            }
            else if (isContent("png", "image/png", res))
            {
                return ".p";
            }
            else if (isContent("gif", "image/gif", res))
            {
                return ".gif";
            }
            else if (isContent("html", "text/html", res))
            {
                return ".html";
            }
            else
            {
                ///
                /// Ok - if there is a filetype that is in the header, use it!
                /// 

                string ext = GetWebResponseFileType(res);
                if (ext != null)
                {
                    return "." + ext;
                }
                return null;
            }
        }

        /// <summary>
        /// Checks the web response to see if the file is of a particular type
        /// </summary>
        /// <param name="filetype"></param>
        /// <param name="mimetype"></param>
        /// <param name="res"></param>
        /// <returns></returns>
        private bool isContent(string filetype, string mimetype, WebResponse res)
        {
            ///
            /// First, inspect the mime-type
            /// 

            if (res.ContentType.Contains(mimetype))
            {
                return true;
            }

            ///
            /// Now, look at the header's filename and look to see if its extention matches
            /// 

            string extension = GetWebResponseFileType(res);
            return extension == filetype;
        }

        /// <summary>
        /// Returns the type of teh file that was in the web response.
        /// </summary>
        /// <param name="res"></param>
        /// <returns></returns>
        private static string GetWebResponseFileType(WebResponse res)
        {
            string extension = null;
            if (res.Headers.AllKeys.Contains("Content-Disposition"))
            {
                var lst = from p in res.Headers["Content-Disposition"].Split(';')
                          where p.Contains("=")
                          select p.Split('=');

                var fname = from p in lst
                            where p[0].Trim().ToLower() == "filename"
                            select p[1].Trim('"');

                if (fname.Count() > 0)
                {
                    string thefilename = fname.Last();

                    extension = Path.GetExtension(thefilename);
                    if (extension[0] == '.')
                    {
                        extension = extension.Substring(1);
                    }
                }
            }
            return extension;
        }

        /// <summary>
        /// Checks the web response to see if the file is of a particular type.
        /// </summary>
        /// <param name="filetype"></param>
        /// <param name="res"></param>
        /// <returns></returns>
        private bool isContent(string filetype, WebResponse res)
        {
            return isContent(filetype, filetype, res);
        }
        
        /// <summary>
        /// We can only do very simple web sites - no password protection, etc. So we
        /// are basically gonig to do only "generic" stuff.
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public RenderPluginPriority RenderPriority(object what)
        {
            ///
            /// We only know how to deal with http right now!
            /// 

            Uri u = what as Uri;
            if (u.Scheme.ToLower() == "http" || u.Scheme.ToLower() == "https")
            {
                return RenderPluginPriority.RPPGeneric;
            }
            return RenderPluginPriority.RPPCantDoIt;
        }

        /// <summary>
        /// Returns the hash value for a Uri. Do it looking at the URI.
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public long GetObjectHash(object what)
        {
            Uri u = what as Uri;
            if (u == null)
            {
                throw new InvalidOperationException("Sorry, can only do a hash for a Uri object, not a " + what.GetType().ToString());
            }
            return u.OriginalString.GetHashCode();
        }

        /// <summary>
        /// Check if two objects are equal. In this case we just check the hashes.
        /// </summary>
        /// <param name="o1"></param>
        /// <param name="o2"></param>
        /// <returns></returns>
        public bool ObjectsEqual(object o1, object o2)
        {
            Uri u1 = o1 as Uri;
            Uri u2 = o2 as Uri;

            if (u1 == null || u2 == null)
            {
                return false;
            }

            return GetObjectHash(u1) == GetObjectHash(u2);
        }

        /// <summary>
        /// Test if things are out of date. Do this by grabbing the modified date.
        /// If we fail, we return false (that is, we loose the ability to tell
        /// when something has changed).
        /// </summary>
        /// <param name="what"></param>
        /// <param name="metaData"></param>
        /// <returns></returns>
        public bool IsOutOfDate(object what, object metaData)
        {
            Uri u = what as Uri;

            if (u == null)
            {
                return false;
            }

            if (metaData == null)
            {
                return false;
            }

            long oldTimeDate = (long)metaData;
            long thisTimeDate = GetCheckpointValue(u);

            return thisTimeDate > oldTimeDate;
        }

        /// <summary>
        /// Gets the object meta data so that we can figure out if we are out of date or not.
        /// </summary>
        /// <param name="what"></param>
        /// <returns></returns>
        public object GetObjectDateMetaData(object what)
        {
            Uri u = what as Uri;
            if (u == null)
            {
                return null;
            }

            ///
            /// See if we can get the value now.
            /// 

            long result = GetCheckpointValue(u);
            if (result == 0)
            {
                return null;
            }
            return result;
        }

        /// <summary>
        /// Gets the last modified time or size. Returns zero if it can't be found.
        /// Tries to use http headers, but if that fails, it will go the extra
        /// step of doing the GET.
        /// </summary>
        /// <param name="u"></param>
        /// <returns></returns>
        private long GetCheckpointValue(Uri u)
        {
            return GetLastModifiedTime(u);
        }

        /// <summary>
        /// Gets the time, in ticks, of the last modification according to the URL.
        /// </summary>
        /// <param name="u"></param>
        /// <returns></returns>
        private long GetLastModifiedTime(Uri u)
        {
            long result = GetLastModifiedTime(u, "HEAD");
            if (result != 0)
            {
                return result;
            }
            result = GetLastModifiedTime(u, "GET");
            if (result != 0)
            {
                return result;
            }
            return 0;
        }

        /// <summary>
        /// Attempt to get the last modified time and return that. Return it as a box'd int. If we fail,
        /// then return null.
        /// </summary>
        /// <param name="u"></param>
        /// <param name="method"></param>
        /// <returns></returns>
        private long GetLastModifiedTime(Uri u, string method)
        {
            WebRequest req = WebRequest.Create(u);
            req.Method = method;
            (req as HttpWebRequest).UserAgent = HttpAgentString;
            try
            {
                using (var res = req.GetResponse())
                {
                    if (res.Headers.AllKeys.Contains("Last-Modified"))
                    {
                        return (long)Convert.ToDateTime(res.Headers["Last-Modified"]).Ticks;
                    }

                    return Convert.ToInt32(res.Headers["Content-Length"]);
                }
            }
            catch
            {
            }
            return 0;
        }
    }
}
